/** * Copyright Alex Objelean */ package ro.isdc.wro.maven.plugin; import static org.apache.commons.lang3.Validate.notNull; import java.io.File; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.apache.maven.artifact.DependencyResolutionRequiredException; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; import org.codehaus.classworlds.ClassRealm; import org.sonatype.plexus.build.incremental.BuildContext; import ro.isdc.wro.WroRuntimeException; import ro.isdc.wro.config.Context; import ro.isdc.wro.extensions.manager.standalone.ExtensionsStandaloneManagerFactory; import ro.isdc.wro.manager.WroManager; import ro.isdc.wro.manager.factory.WroManagerFactory; import ro.isdc.wro.manager.factory.standalone.StandaloneContext; import ro.isdc.wro.manager.factory.standalone.StandaloneContextAware; import ro.isdc.wro.maven.plugin.support.ExtraConfigFileAware; import ro.isdc.wro.maven.plugin.support.ResourceChangeHandler; import ro.isdc.wro.model.WroModel; import ro.isdc.wro.model.WroModelInspector; import ro.isdc.wro.model.group.Group; import ro.isdc.wro.model.resource.Resource; import ro.isdc.wro.util.concurrent.TaskExecutor; import com.google.common.annotations.VisibleForTesting; /** * Defines most common properties used by wro4j build-time solution infrastructure. * * @author Alex Objelean */ public abstract class AbstractWro4jMojo extends AbstractMojo { /** * File containing the groups definitions. * * @parameter default-value="${basedir}/src/main/webapp/WEB-INF/wro.xml" property="wroFile" * @optional */ private File wroFile; /** * Allows clients to pass a build-time parameter to skip the plugin execution.. * * @parameter default-value=false * @optional */ private boolean skip; /** * The folder where web application context resides useful for locating resources relative to servletContext. It is * possible to provide multiple context folders using a CSV. When multiple contextFolders are provided, the * servletContext locator will try to search in next contextFolder when a resource could not be located. By default, a * single context folder is configured. * * @parameter default-value="${basedir}/src/main/webapp/" property="contextFolder" * @optional */ private String contextFolder; /** * @parameter default-value="true" property="minimize" * @optional */ private boolean minimize; /** * @parameter property="ignoreMissingResources" * @optional */ private String ignoreMissingResources; /** * Comma separated group names. This field is optional. If no value is provided, a file for each group will be * created. * * @parameter property="targetGroups" * @optional */ private String targetGroups; /** * @parameter default-value="${project}" */ private MavenProject mavenProject; /** * @parameter property="wroManagerFactory" * @optional */ private String wroManagerFactory; /** * An instance of {@link StandaloneContextAware}. */ private WroManagerFactory managerFactory; /** * The path to configuration file. * * @parameter default-value="${basedir}/src/main/webapp/WEB-INF/wro.properties" property="extraConfigFile" * @optional */ private File extraConfigFile; /** * Responsible for identifying the resources changed during incremental build. * <p/> * Read more about it <a href="http://wiki.eclipse.org/M2E_compatible_maven_plugins#BuildContext">here</a> * * @component */ private BuildContext buildContext; /** * This parameter is not meant to be used. The only purpose is to hold project build directory * * @parameter default-value="${project.build.directory}" * @optional */ private File buildDirectory; /** * When this flag is enabled and there are more than one group to be processed, these will be processed in parallel, * resulting in faster overall plugin execution time. * * @parameter default-value="false" property="parallelProcessing" * @optional */ private boolean parallelProcessing; /** * Flag which allows to enable incremental build (experimental feature). It is false by default, but probably can be * changed to true if no unexpected problems are detected.. * * @parameter default-value="false" property="incrementalBuildEnabled" * @optional */ private boolean incrementalBuildEnabled; private TaskExecutor<Void> taskExecutor; private ResourceChangeHandler resourceChangeHandler; public final void execute() throws MojoExecutionException { if (skip) { getLog().info("Skipping execution."); } else { validate(); if (buildDirectory == null) { buildDirectory = new File(mavenProject.getModel().getBuild().getDirectory()); } getLog().info(contextFolder); getLog().info("Executing the mojo: "); getLog().info("Wro4j Model path: " + wroFile.getPath()); getLog().info("targetGroups: " + getTargetGroups()); getLog().info("minimize: " + isMinimize()); getLog().info("ignoreMissingResources: " + isIgnoreMissingResources()); getLog().info("parallelProcessing: " + isParallelProcessing()); getLog().info("buildDirectory: " + buildDirectory); getLog().debug("wroManagerFactory: " + wroManagerFactory); getLog().debug("incrementalBuildEnabled: " + incrementalBuildEnabled); getLog().debug("extraConfig: " + extraConfigFile); extendPluginClasspath(); Context.set(Context.standaloneContext()); try { onBeforeExecute(); doExecute(); } catch (final Exception e) { final String message = "Exception occured while processing: " + e.toString() + ", class: " + e.getClass().getName() + ",caused by: " + (e.getCause() != null ? e.getCause().getClass().getName() : ""); getLog().error(message, e); if (e instanceof WroRuntimeException) { // Do not keep resources which cause the exception. This is helpful for linter processors. final Resource resource = ((WroRuntimeException) e).getResource(); forgetResource(resource); } throw new MojoExecutionException(message, e); } finally { try { onAfterExecute(); } catch (final Exception e) { throw new MojoExecutionException("Exception in onAfterExecute", e); } } } } /** * Safely invoke {@link ResourceChangeHandler#forget(Resource)}. The safety is required because invoking * {@link #getResourceChangeHandler()} can throw an exception during initialization. */ private void forgetResource(final Resource resource) { if (resourceChangeHandler != null) { resourceChangeHandler.forget(resource); } } /** * Creates a {@link StandaloneContext} by setting properties passed after mojo is initialized. */ private StandaloneContext createStandaloneContext() { final StandaloneContext runContext = new StandaloneContext(); runContext.setContextFoldersAsCSV(getContextFoldersAsCSV()); runContext.setMinimize(isMinimize()); runContext.setWroFile(getWroFile()); runContext.setIgnoreMissingResourcesAsString(isIgnoreMissingResources()); return runContext; } /** * Perform actual plugin processing. */ protected abstract void doExecute() throws Exception; /** * This method will ensure that you have a right and initialized instance of {@link StandaloneContextAware}. When * overriding this method, ensure that creating managerFactory performs injection during manager creation, otherwise * the manager won't be initialized properly. * * @return {@link WroManagerFactory} implementation. */ protected WroManagerFactory getManagerFactory() { if (managerFactory == null) { WroManagerFactory localManagerFactory = null; try { localManagerFactory = newWroManagerFactory(); } catch (final MojoExecutionException e) { throw WroRuntimeException.wrap(e); } // initialize before process. if (localManagerFactory instanceof StandaloneContextAware) { ((StandaloneContextAware) localManagerFactory).initialize(createStandaloneContext()); } managerFactory = decorateManagerFactory(localManagerFactory); } return managerFactory; } /** * Allows the initialized manager factory to be decorated. */ protected WroManagerFactory decorateManagerFactory(final WroManagerFactory managerFactory) { return managerFactory; } /** * {@inheritDoc} */ protected WroManagerFactory newWroManagerFactory() throws MojoExecutionException { WroManagerFactory factory = null; if (wroManagerFactory != null) { factory = createCustomManagerFactory(); } else { factory = new ExtensionsStandaloneManagerFactory(); } getLog().info("wroManagerFactory class: " + factory.getClass().getName()); if (factory instanceof ExtraConfigFileAware) { if (extraConfigFile == null) { throw new MojoExecutionException("The " + factory.getClass() + " requires a valid extraConfigFile!"); } getLog().debug("Using extraConfigFile: " + extraConfigFile.getAbsolutePath()); ((ExtraConfigFileAware) factory).setExtraConfigFile(extraConfigFile); } return factory; } /** * Creates an instance of Manager factory based on the value of the wroManagerFactory plugin parameter value. */ private WroManagerFactory createCustomManagerFactory() throws MojoExecutionException { WroManagerFactory managerFactory; try { final Class<?> wroManagerFactoryClass = Thread.currentThread().getContextClassLoader().loadClass( wroManagerFactory.trim()); managerFactory = (WroManagerFactory) wroManagerFactoryClass.newInstance(); } catch (final Exception e) { throw new MojoExecutionException("Invalid wroManagerFactoryClass, called: " + wroManagerFactory, e); } return managerFactory; } /** * @return a list of groups which will be processed. */ protected final List<String> getTargetGroupsAsList() throws Exception { List<String> result = null; if (isIncrementalCheckRequired()) { result = getIncrementalGroupNames(); } else if (getTargetGroups() == null) { result = getAllModelGroupNames(); } else { result = Arrays.asList(getTargetGroups().split(",")); } persistResourceFingerprints(result); if (result.isEmpty()) { getLog().info("Nothing to process (nothing configured or nothing changed since last build)."); } else { getLog().info("The following groups will be processed: " + result); } return result; } /** * Store digest for all resources contained inside the list of provided groups. */ private void persistResourceFingerprints(final List<String> groupNames) { final WroModelInspector modelInspector = new WroModelInspector(getModel()); for (final String groupName : groupNames) { final Group group = modelInspector.getGroupByName(groupName); if (group != null) { for (final Resource resource : group.getResources()) { getResourceChangeHandler().remember(resource); } } } } /** * @return a list of groups changed by incremental builds. */ private List<String> getIncrementalGroupNames() throws Exception { final List<String> changedGroupNames = new ArrayList<String>(); for (final Group group : getModel().getGroups()) { // skip processing non target groups if (isTargetGroup(group)) { for (final Resource resource : group.getResources()) { getLog().debug("checking delta for resource: " + resource); if (getResourceChangeHandler().isResourceChanged(resource)) { getLog().debug("detected change for resource: " + resource + " and group: " + group.getName()); changedGroupNames.add(group.getName()); // no need to check rest of resources from this group break; } } } } return changedGroupNames; } /** * Check if the provided group is a target group. */ private boolean isTargetGroup(final Group group) { notNull(group); final String targetGroups = getTargetGroups(); // null, means all groups are target groups return targetGroups == null || targetGroups.contains(group.getName()); } /** * Checks if all required fields are configured. */ protected void validate() throws MojoExecutionException { if (wroFile == null) { throw new MojoExecutionException("contextFolder was not set!"); } if (contextFolder == null) { throw new MojoExecutionException("no contextFolder was set!"); } } /** * Update the classpath. */ protected final void extendPluginClasspath() throws MojoExecutionException { // this code is inspired from http://teleal.org/weblog/Extending%20the%20Maven%20plugin%20classpath.html final List<String> classpathElements = new ArrayList<String>(); try { classpathElements.addAll(mavenProject.getRuntimeClasspathElements()); } catch (final DependencyResolutionRequiredException e) { throw new MojoExecutionException("Could not get compile classpath elements", e); } final ClassLoader classLoader = createClassLoader(classpathElements); Thread.currentThread().setContextClassLoader(classLoader); } /** * @return {@link ClassRealm} based on project dependencies. */ private ClassLoader createClassLoader(final List<String> classpathElements) { getLog().debug("Classpath elements:"); final List<URL> urls = new ArrayList<URL>(); try { for (final String element : classpathElements) { final File elementFile = new File(element); getLog().debug("Adding element to plugin classpath: " + elementFile.getPath()); urls.add(elementFile.toURI().toURL()); } } catch (final Exception e) { getLog().error("Error retreiving URL for artifact", e); throw new RuntimeException(e); } return new URLClassLoader(urls.toArray(new URL[] {}), Thread.currentThread().getContextClassLoader()); } /** * @return The {@link TaskExecutor} responsible for running multiple tasks in parallel. */ protected final TaskExecutor<Void> getTaskExecutor() { if (taskExecutor == null) { taskExecutor = new TaskExecutor<Void>() { @Override protected void onException(final Exception e) { // propagate exception throw WroRuntimeException.wrap(e); } }; } return taskExecutor; } /** * @return true if the only incremental changed group should be used as target groups for next processing. */ protected boolean isIncrementalCheckRequired() { return isIncrementalBuild(); } /** * Invoked before execution is performed. */ protected void onBeforeExecute() { } /** * Invoked right after execution completion. This method is invoked also if the execution failed with an exception. */ protected void onAfterExecute() { resourceChangeHandler.persist(); } /** * @return true if the build was triggered by an incremental change. */ protected final boolean isIncrementalBuild() { return getResourceChangeHandler().isIncrementalBuild(); } private List<String> getAllModelGroupNames() { return new WroModelInspector(getModel()).getGroupNames(); } private WroModel getModel() { return getWroManager().getModelFactory().create(); } private WroManager getWroManager() { try { return getManagerFactory().create(); } catch (final Exception e) { throw WroRuntimeException.wrap(e); } } private ResourceChangeHandler getResourceChangeHandler() { if (resourceChangeHandler == null) { resourceChangeHandler = ResourceChangeHandler.create(getManagerFactory(), getLog()).setBuildContext(buildContext).setBuildDirectory( buildDirectory).setIncrementalBuildEnabled(incrementalBuildEnabled); } return resourceChangeHandler; } @VisibleForTesting void setTaskExecutor(final TaskExecutor<Void> taskExecutor) { this.taskExecutor = taskExecutor; } /** * @param contextFolder * the servletContextFolder to set * @VisibleForTesting */ String getContextFoldersAsCSV() { return contextFolder; } /** * @param contextFolders * a CSV representing contextFolders to use. * @VisibleForTesting */ void setContextFolder(final String contextFolder) { this.contextFolder = contextFolder; } /** * @param wroFile * the wroFile to set * @VisibleForTesting */ void setWroFile(final File wroFile) { this.wroFile = wroFile; } /** * @return the wroFile * @VisibleForTesting */ File getWroFile() { return this.wroFile; } /** * @param minimize * flag for minimization. * @VisibleForTesting */ void setMinimize(final boolean minimize) { this.minimize = minimize; } /** * @param ignoreMissingResourcesAsString * the ignoreMissingResources to set * @VisibleForTesting */ void setIgnoreMissingResources(final String ignoreMissingResourcesAsString) { this.ignoreMissingResources = ignoreMissingResourcesAsString; } void setIgnoreMissingResources(final boolean ignoreMissingResources) { setIgnoreMissingResources(Boolean.toString(ignoreMissingResources)); } /** * @VisibleForTesting */ protected final boolean isParallelProcessing() { return parallelProcessing; } /** * @VisibleForTesting */ final void setParallelProcessing(final boolean parallelProcessing) { this.parallelProcessing = parallelProcessing; } /** * @VisibleForTesting */ void setIncrementalBuildEnabled(final boolean incrementalBuildEnabled) { this.incrementalBuildEnabled = incrementalBuildEnabled; } /** * @return the minimize * @VisibleForTesting */ boolean isMinimize() { return this.minimize; } /** * @return the ignoreMissingResources * @VisibleForTesting */ String isIgnoreMissingResources() { return this.ignoreMissingResources; } /** * Used for testing. * * @param mavenProject * the mavenProject to set */ void setMavenProject(final MavenProject mavenProject) { this.mavenProject = mavenProject; } /** * @return the targetGroups * @VisibleForTesting */ String getTargetGroups() { return this.targetGroups; } /** * @param versionEncoder * (targetGroups) comma separated group names. * @VisibleForTesting */ void setTargetGroups(final String targetGroups) { this.targetGroups = targetGroups; } /** * @param wroManagerFactory * fully qualified name of the {@link WroManagerFactory} class. * @VisibleForTesting */ void setWroManagerFactory(final String wroManagerFactory) { this.wroManagerFactory = wroManagerFactory; } /** * @param extraConfigFile * the extraConfigFile to set * @VisibleForTesting */ void setExtraConfigFile(final File extraConfigFile) { this.extraConfigFile = extraConfigFile; } /** * @VisibleForTesting */ void setBuildContext(final BuildContext buildContext) { this.buildContext = buildContext; } /** * @VisibleForTesting */ void setSkip(final boolean skip) { this.skip = skip; } /** * Removes any persisted data creating during the build. * * @VisibleForTesting */ void clean() { try { getResourceChangeHandler().destroy(); } catch (final Exception e) { // do not propagate the error during cleanup getLog().error("Failed to destroy resourceChangeHandler", e); } } }